using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Serialization;

namespace TestCases.Utils.Cloning
{
    public class ObjectCloner
    {

        private readonly bool _isDeepCopy;
        private readonly Stack<object> _parents;// = new Stack<object>();
            
        public ObjectCloner(bool isDeepCopy)
        {
            _isDeepCopy = isDeepCopy;
            if (_isDeepCopy)
            {
                _parents = new Stack<object>();
            }
        }

        private static bool IsSimpleType(Type type)
        {
            return type.IsPrimitive || type.IsEnum || type == typeof(string);
        }

        private void CopyArrayItems(Array source, Array target, long[] indexes, int dimension)
        {

            long dimensionLength = source.GetLongLength(dimension);
            if (dimension == source.Rank - 1)
            {
                for (int i = 0; i < dimensionLength; i++)
                {
                    indexes[dimension] = i;
                        
                    var sourceValue = source.GetValue(indexes);
                    target.SetValue((_isDeepCopy) ? CopyObject(sourceValue) : sourceValue, indexes);
                        
                }                
            }
            else
            {
                for (int i = 0; i < dimensionLength; i++)
                {
                        
                    indexes[dimension] = i;
                    CopyArrayItems(source, target, indexes, dimension + 1);
                        
                }                
            }
        }
        
        private Array CopyArray(Array array, Type type)
        {
            var lengths = new long[array.Rank];
            for (int dimension = 0; dimension < lengths.Length; dimension++)
            {
                lengths[dimension] = array.GetLongLength(dimension);
            }

            var arrayCopy = Array.CreateInstance(type.GetElementType(), lengths);

            // array of lengths is used as array of indexes to save memory :)
            // var indexes = new long[array.Rank];
            CopyArrayItems(array, arrayCopy, lengths /* indexes */, 0);
            return arrayCopy;
        }
        
        private object CopyFields(object obj, Type type)
        {
            
            // If it is a class or struct then i do field-by-field deep/shallow copy.
            // Traversing over structs is not necessarily when struct doesn't contains reference types... but it can
            var copy = FormatterServices.GetUninitializedObject(type);
            foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
            {
                var sourceValue = field.GetValue(obj);
                field.SetValue(copy, (_isDeepCopy) ? CopyObject(sourceValue) : sourceValue);
            }
            return copy;

        }

        private object CopyComplex(object obj, Type type)
        {
            return (type.IsArray) ? CopyArray((Array) obj, type) : CopyFields(obj, type);
        }


        public object CopyObject(object obj)
        {
            if (obj == null)
            {
                return null;
            }

            var type = obj.GetType();
            if (IsSimpleType(type))
            {
                return obj;
            }

            if (!_isDeepCopy)
            {
                return CopyComplex(obj, type);
            }


            // in deep copy i need to detect circular references...
            if (_parents.Contains(obj))
            {
                return obj;
            }            
            _parents.Push(obj);
            var result = CopyComplex(obj, type);
            _parents.Pop();
                     
            return result;

        }
    }
}